﻿namespace Legend
{
    using System;
    using System.IO;
    using System.Text;

    /// <summary>
    /// A text writer where output can be indented.
    /// </summary>
    public class IndentedTextWriter
        : TextWriter
    {
        private const string DefaultIndentation = "    ";
        private readonly string indentation;
        private readonly TextWriter output;
        private int indentLevel;
        private bool isAtNewLine;

        /// <summary>
        /// Initializes a new instance of the <see cref="IndentedTextWriter"/> class.
        /// </summary>
        public IndentedTextWriter()
            : this(new StringWriter())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="IndentedTextWriter"/> class.
        /// </summary>
        /// <param name="output">The output of the writer.</param>
        public IndentedTextWriter(StringBuilder output)
            : this(new StringWriter(output))
        {
        }

        /// <summary>
        /// Creates a new IndentedTextWriter that writes to the specified output.
        /// </summary>
        /// <param name="output">The output writer.</param>
        public IndentedTextWriter(TextWriter output)
            : this(DefaultIndentation, output)
        {
        }

        /// <summary>
        /// Creates a new IndentedTextWriter that writes to the specified output.
        /// </summary>
        /// <param name="output">The output writer.</param>s
        /// <param name="indentation">The string sto use for indentation.</param>
        /// <exception cref="ArgumentNullException">The indentation is null.</exception>
        public IndentedTextWriter(string indentation, TextWriter output)
        {
            GuardAgainstNull(indentation, "indentation");
            GuardAgainstNull(output, "output");

            this.indentation = indentation;
            this.output = output;
            this.isAtNewLine = true;
            this.indentLevel = 0;
        }

        /// <summary>
        /// When overridden in a derived class, returns the <see cref="T:System.Text.Encoding"/> in which the output is written.
        /// </summary>
        /// <returns>The Encoding in which the output is written.</returns>
        public override Encoding Encoding
        {
            get { return this.output.Encoding; }
        }

        /// <summary>
        /// Gets an object that controls formatting.
        /// </summary>
        /// <returns>An <see cref="T:System.IFormatProvider"/> object for a specific culture, or the formatting of the current culture if no other culture is specified.</returns>
        public override IFormatProvider FormatProvider
        {
            get { return this.output.FormatProvider; }
        }

        /// <summary>
        /// Gets or sets the line terminator string used by the current TextWriter.
        /// </summary>
        /// <returns>The line terminator string for the current TextWriter.</returns>
        public override string NewLine
        {
            get { return this.output.NewLine; }
            set { this.output.NewLine = value; }
        }

        /// <summary>
        /// Makes the console indentable.
        /// </summary>
        /// <returns>An object that can be used to indent the console output.</returns>
        public static IndentedTextWriter MakeConsoleIndentable()
        {
            var writer = new IndentedTextWriter(Console.Out);
            Console.SetOut(writer);
            return writer;
        }

        /// <summary>
        /// Increases the indent level and returns an instance that can be used to unindent.
        /// </summary>
        /// <returns>An object that can be used to unindent.</returns>
        public IDisposable Indent()
        {
            this.indentLevel++;
            return new Unindenter(this);
        }

        /// <summary>
        /// Clears all buffers for the current writer and causes any buffered data to be written to the underlying device.
        /// </summary>
        public override void Flush()
        {
            this.output.Flush();
        }

        /// <summary>
        /// Closes the current writer and releases any system resources associated with the writer.
        /// </summary>
        public override void Close()
        {
            this.output.Close();
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return this.output.ToString();
        }

        /// <summary>
        /// Writes a character to the text stream.
        /// </summary>
        /// <param name="value">The character to write to the text stream.</param>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.IO.TextWriter"/> is closed. </exception>
        ///   
        /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
        public override void Write(char value)
        {
            this.WriteIndentationWhenAtNewLine(value);

            this.output.Write(value);
        }

        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="T:System.IO.TextWriter"/> and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.output.Dispose();
            }
        }

        private void WriteIndentationWhenAtNewLine(char value)
        {
            if (this.isAtNewLine)
            {
                this.WriteIndentation();
            }

            this.isAtNewLine = value.Equals('\n');
        }

        private void WriteIndentation()
        {
            for (int i = 0; i < this.indentLevel; i++)
            {
                this.output.Write(this.indentation);
            }
        }

        private static void GuardAgainstNull(object value, string argumentName)
        {
            if (value == null)
            {
                throw new ArgumentNullException(argumentName);
            }
        }

        private class Unindenter
            : IDisposable
        {
            private readonly IndentedTextWriter writer;
            private bool isDisposed;

            public Unindenter(IndentedTextWriter writer)
            {
                this.writer = writer;
                this.isDisposed = false;
            }

            public void Dispose()
            {
                if (!this.isDisposed)
                {
                    this.isDisposed = true;
                    this.writer.indentLevel--;
                }
            }
        }
    }
}